#include "GL/GLShaderProgram.h"
#include "Logger.h"
#include "Exception.h"
#include <sstream>
#include "Vertex.h"
#include "tinyxml2.h"
#include "IFileSystem.h"
#include "GL/GLTexture.h"
#include "IContext.h"

NS_B2D_BEGIN

GLShader::GLShader(ShaderType type)
	: Shader(type)
	, mHandle(0)
{
	create();
}

GLShader::~GLShader(void)
{
	destroy();
}

void GLShader::create(void)
{
	if( isInitialised() )
		destroy();

	// Create shader
	GLenum type = getGLType(mType);
	mHandle = glCreateShader(type);
	if( mHandle == 0 )
	{
		B2D_THROW(Exception::GRAPHICS_API_EXCEPTION,
			"Could not create shader.");
	}
	B2D_LOG_DEBUG("Shader created with id(%i)\n", mHandle);
}

void GLShader::destroy(void)
{
	// Check if we actually need to do something
	if( isInitialised() == false )
		return;

	glDeleteShader(mHandle);
	B2D_LOG_DEBUG("Shader id(%i) was destroyed\n", mHandle);
}

GLenum GLShader::getGLType(ShaderType type)
{
	switch( type )
	{
	case ShaderType::FRAGMENT :
		return GL_FRAGMENT_SHADER;
	default :
		return GL_VERTEX_SHADER;
	}
}

void GLShader::compileImpl(void)
{
	// Set data
	glShaderSource(mHandle, 1, (const GLchar**)&mSource, (const GLint*)&mSourceSize);

	// Compile
	glCompileShader(mHandle);
	GLint compiled;
	glGetShaderiv(mHandle, GL_COMPILE_STATUS, &compiled);
		
	if(compiled != GL_TRUE)
	{
		GLint infoLen = 0;
		glGetShaderiv(mHandle, GL_INFO_LOG_LENGTH, &infoLen);
		if(infoLen > 1)
		{
			// Get error
			char* infoLog = new char[infoLen];
			glGetShaderInfoLog(mHandle, infoLen, nullptr, infoLog);
			// Format error
			std::stringstream ss;
			ss << "Could not compile shader with id(" << mHandle << "): " << infoLog;
			SafeDeleteArray(infoLog);
			// It is acceptable to throw?
			B2D_THROW(Exception::GRAPHICS_API_EXCEPTION,
				ss.str() );
		}
	}

	B2D_LOG_DEBUG("Shader with id(%i) was compiled\n", mHandle);
}


//
// ShaderProgram
//

struct TempEditBind
{
	GLint tmpName;
	bool restore;
	TempEditBind(GLuint spName)
		: restore(false)
	{
		glGetIntegerv(GL_CURRENT_PROGRAM, &tmpName);
		if( tmpName != spName )
		{
			glUseProgram(spName);
			restore = true;
		}
	}
	~TempEditBind(void)
	{
		if( restore == true )
			glUseProgram(tmpName);
	}
};

#define GET_UNIFORM_OR_RETURN(location, name) \
location = getUniformLocation(name); \
if( location < 0 ) \
{ \
	B2D_LOG_ERROR("Tried setting uniform %s which doesnt exist\n", name.c_str()); \
	return; \
} \
TempEditBind tmpBind(mHandle);


B2D_OBJECT_IMPL(GLShaderProgram, ShaderProgram)

GLShaderProgram::GLShaderProgram(void)
	: mHandle(0)
{

}

GLShaderProgram::~GLShaderProgram(void)
{
	// Remove shaders
	for(size_t i = 0; i < ShaderTypeCount; ++i)
	{
		removeShader((ShaderType)i);
	}

	// Delete program
	glDeleteProgram(mHandle);
	B2D_LOG_DEBUG("Shader program id(%i) was unloaded\n", mHandle);
}


Shader* GLShaderProgram::addShaderImpl(ShaderType type)
{
	TempEditBind tmpBind(mHandle);
	// Create and attach
	Shader* shader =  new GLShader(type);
	glAttachShader(mHandle, reinterpret_cast<GLuint>(shader->getHandle()));
	return shader;
}

void GLShaderProgram::removeShaderImpl(Shader* shader)
{
	TempEditBind tmpBind(mHandle);
	glDetachShader(mHandle, reinterpret_cast<GLuint>(shader->getHandle()) );
}

void GLShaderProgram::linkImpl(void)
{
	// Set uniform locations
	glBindAttribLocation(mHandle, VertexAttribute::ATTRIBUTE_POSITION,	"a_position");
	glBindAttribLocation(mHandle, VertexAttribute::ATTRIBUTE_NORMAL,	"a_normal");
	glBindAttribLocation(mHandle, VertexAttribute::ATTRIBUTE_COLOR,		"a_color");
	glBindAttribLocation(mHandle, VertexAttribute::ATTRIBUTE_TEXCOORD0, "a_texCoord0");
	glBindAttribLocation(mHandle, VertexAttribute::ATTRIBUTE_TEXCOORD1, "a_texCoord1");

	// Link
	glLinkProgram(mHandle);
	GLint linked;
	glGetProgramiv(mHandle, GL_LINK_STATUS, &linked);
	if(linked != GL_TRUE) // Not linked?
	{
		GLint infoLen = 0;
		glGetProgramiv(mHandle, GL_INFO_LOG_LENGTH, &infoLen);
		if(infoLen > 1)
		{
			char* infoLog = new char[infoLen];
			glGetProgramInfoLog(mHandle, infoLen, nullptr, infoLog);
			std::stringstream ss;
			ss << "Error linking program with id(" << mHandle << "): " << infoLog;
			SafeDeleteArray(infoLog);
			B2D_THROW(Exception::GRAPHICS_API_EXCEPTION, ss.str());
		}
	}

	// Get uniforms
	retrieveUniforms();
	// Get number of attributes
	int attrCount = 0;
	glGetProgramiv(mHandle, GL_ACTIVE_ATTRIBUTES, &attrCount);

	B2D_LOG_DEBUG("Shader program with id(%i) linked with %i attributes and %i uniforms: \n", mHandle, attrCount, mUniforms.size());
}

void GLShaderProgram::retrieveUniforms(void)
{
	// Clear uniforms
	mUniforms.clear();
	
	// Get number uniforms
	GLint varCount = 0;
	glGetProgramiv(mHandle, GL_ACTIVE_UNIFORMS, &varCount);
	if( 0 == varCount ) return;
		
	GLchar varName[MAX_UNIFORM_NAME_LENGTH];
	for(size_t index = 0; index < (size_t)varCount; ++index)
	{
		memset(varName, 0, MAX_UNIFORM_NAME_LENGTH);
			
		// Get the Attrib Info
		GLint size;
		GLenum variableType;
		glGetActiveUniform(mHandle, (GLuint)index, MAX_UNIFORM_NAME_LENGTH, nullptr, &size, &variableType, varName);
		
		GLuint location = glGetUniformLocation(mHandle, varName);
		mUniforms.insert( UniformMap::value_type(string(varName), location) );
	}
}

GLint GLShaderProgram::getUniformLocation(const string& name) const
{
	const UniformMap::const_iterator it = mUniforms.find(name);
	return (it == mUniforms.end() ? -1 : it->second);
}

void GLShaderProgram::setUniform1i(const string& name, int value)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform1i(location, value);

}

void GLShaderProgram::setUniform1f(const string& name, float value)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform1f(location, value);
}

void GLShaderProgram::setUniform2i(const string& name, int value0, int value1)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform2i(location, value0, value1);
}

void GLShaderProgram::setUniform2f(const string& name, float value0, float value1)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform2f(location, value0, value1);
}

void GLShaderProgram::setUniform3i(const string& name, int value0, int value1, int value2)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform3i(location, value0, value1, value2);
}

void GLShaderProgram::setUniform3f(const string& name, float value0, float value1, float value2)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform3f(location, value0, value1, value2);
}

void GLShaderProgram::setUniform4i(const string& name, int value0, int value1, int value2, int value3)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform4i(location, value0, value1, value2, value3);
}

void GLShaderProgram::setUniform4f(const string& name, float value0, float value1, float value2, float value3)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform4f(location, value0, value1, value2, value3);
}

void GLShaderProgram::setUniform1iv(const string& name, size_t count, const int* values)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform1iv(location, count, values);
}

void GLShaderProgram::setUniform2iv(const string& name, size_t count, const int* values)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform2iv(location, count, values);
}

void GLShaderProgram::setUniform3iv(const string& name, size_t count, const int* values)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform3iv(location, count, values);
}

void GLShaderProgram::setUniform4iv(const string& name, size_t count, const int* values)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform4iv(location, count, values);
}

void GLShaderProgram::setUniform1fv(const string& name, size_t count, const float* values)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform1fv(location, count, values);
}

void GLShaderProgram::setUniform2fv(const string& name, size_t count, const float* values)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform2fv(location, count, values);
}

void GLShaderProgram::setUniform3fv(const string& name, size_t count, const float* values)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform3fv(location, count, values);
}

void GLShaderProgram::setUniform4fv(const string& name, size_t count, const float* values)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniform4fv(location, count, values);
}

void GLShaderProgram::setUniformMat2fv(const string& name, size_t count, bool transpose, const float* values)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniformMatrix2fv(location, count, transpose, values);
}

void GLShaderProgram::setUniformMat3fv(const string& name, size_t count, bool transpose, const float* values)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniformMatrix3fv(location, count, transpose, values);
}

void GLShaderProgram::setUniformMat4fv(const string& name, size_t count, bool transpose, const float* values)
{
	GLint location;
	GET_UNIFORM_OR_RETURN(location, name);
	glUniformMatrix4fv(location, count, transpose, values);
}

void GLShaderProgram::setUniformSampler(const string& name, const ITexture& sampler)
{
	this->setUniform1i(name, static_cast<const GLTexture&>(sampler).getGLName());
}

void GLShaderProgram::load(const StreamPtr& stream)
{
	// Create program
	mHandle = glCreateProgram();
	if( mHandle == 0 )
	{
		B2D_THROW(Exception::GRAPHICS_API_EXCEPTION,
			"Could not create shader program.");
	}
	B2D_LOG_DEBUG("Shader program created with id(%i)\n", mHandle);

	// Check stream
	if( stream.get() == nullptr )
		B2D_THROW(Exception::NULL_ALRGUMENT_EXCEPTION,
		"Cant load from null stream.");
	stream->open(AccessMode::READ);
	string dataStr;
	dataStr.resize(stream->getSize());
	stream->read(&dataStr[0], dataStr.size());
	stream->close();

	// Create document
	tinyxml2::XMLDocument doc;
	doc.Parse(dataStr.c_str(), dataStr.size());

	// Check for correct format
	tinyxml2::XMLElement* root = doc.FirstChildElement("Resource");
	if( root == nullptr || root->Attribute("type") != "ShaderProgram" )
	{
		B2D_THROW(Exception::PARSING_EXCEPTION,
			"The resource \""+mName.getString()+"\" is not a shader program");
	}

	tinyxml2::XMLElement* xml_shader = root->FirstChildElement("VertexShader");
	if( xml_shader != nullptr )
	{
		const char* src = xml_shader->GetText();
		IShader* shader = addShader(ShaderType::VERTEX);
		shader->compileWithSource(src);
	}
	xml_shader = root->FirstChildElement("FragmentShader");
	if( xml_shader != nullptr )
	{
		const char* src = xml_shader->GetText();
		IShader* shader = addShader(ShaderType::FRAGMENT);
		shader->compileWithSource(src);
	}

	link();
}


size_t GLShaderProgram::getSizeEstimate(void) const
{
	// TODO SP
	return 0;
}

NS_B2D_END